package edu.mercer.sse.ldap;

import edu.mercer.sse.app.Config;

import java.security.GeneralSecurityException;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPSearchException;
import com.unboundid.ldap.sdk.SearchResultEntry;
import com.unboundid.ldap.sdk.SearchScope;

import edu.mercer.sse.database.DB2LDAPLayout;
import edu.mercer.sse.errors.CriticalException;
import edu.mercer.sse.errors.CustomLogger;
import edu.mercer.sse.errors.LoggerCustom;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * 
 * LDAP Organizational Unit: 'ou'.<p>
 * 
 * Downloads all entries belonging to an 'ou'. Retrieves only the LDAP 
 * 	attributes specified in database->LDAP layout.
 *  
 * @see DB2LDAPLayout
 */

public class LDAPOrgUnit
{
	private static Optional<ImmutableList<ImmutableMap<String,String>>> 
		_ldapOUusers = Optional.absent();

	private static Optional<ImmutableList<SearchResultEntry>> 
		_ldapOUentries = Optional.absent();

	private static final CustomLogger _log = LoggerCustom.obtainLog( 
			
			"LDAPOrgUnit", 
				
			Config.getLogLevel());
	
	/**
	 * 
	 * LDAP Organizational Unit: 'ou'.<p>
	 * 
	 * Constructor.<p>
	 * 
	 * Downloads all entries belonging to an 'ou'. Retrieves only the LDAP 
	 * 	attributes specified in database->LDAP layout.
	 *  
	 * @see DB2LDAPLayout
	 */
	
	public LDAPOrgUnit() throws GeneralSecurityException,
							    LDAPSearchException, 
						   	    LDAPException, 
						   	    SQLException
	{
		if( ! _ldapOUusers.isPresent() ) downloadOU();
						
	}//constructor

	
	/**
	 * Downloads LDAP Org Unit members and their attributes.<p>
	 * 
	 * Filters member list by applying unique group of names as specified in
	 * 	configuration.<p>
	 * 
	 * @return  ImmutableList&#60;ImmutableMap&#60;String,String&#62;&#62; - 
	 * 	read-only List of read-only Maps containing entries from LDAP OU
	 * 
	 * @throws GeneralSecurityException  error obtaining SSL socket for 
	 * 	secure communication with LDAP
	 * 
	 * @throws LDAPSearchException  OU not found in LDAP or other search 
	 * 	error
	 * 
	 * @throws LDAPException  invalid LDAP search syntax, LDAP connectivity 
	 * 	or other error
	 * 
	 * @throws SQLException  error accessing database for database->LDAP 
	 * 	layout
	 *
	 * @see Config
	 */
	
	private synchronized static ImmutableList<ImmutableMap<String,String>> 
		
		downloadOU() throws GeneralSecurityException,
			   				LDAPSearchException, 
			   				LDAPException,
			   				SQLException
	{	
		if( _ldapOUusers.isPresent() ) return _ldapOUusers.get();
		
		/* perform LDAP search, getting entries in ou */

		/* 
		 * fetch needed LDAP attributes from database->ldap layout as array 
		 * for the ldap search var arg
		 * 
		 */
		
		final Iterator<SearchResultEntry> ouUsers = 
				
				LDAPConnector.con().search
					 
				   (Config.settings().getProperty( "ldap-entries-ou" ) 
						   
					+ "," +

					Config.settings().getProperty( "ldap-base-dn" ),

					SearchScope.SUB,
					
					Config.settings().getProperty( "ldap-entries-filter" ),
					
					DB2LDAPLayout.getLayout()
					 			 .values()
					 			 .toArray( new String[ DB2LDAPLayout
					 			                       .getLayout()
					 			                       .values()
					 			                       .size() ] ))
						   
			         .getSearchEntries()
					 .iterator();

		if( !ouUsers.hasNext() ) throw new CriticalException( 
				"ldap ou: empty search results" );

		/* create collection of LDAP SearchResultEntry's */
		
		final List<SearchResultEntry> uidsLdapFormat = Lists.newArrayList();
		
		/* convert into Map collection, same as database record set */
		
		List<ImmutableMap<String,String>> usersDbFormat = Lists
				.newArrayList();
		
		final String uidStr = Config.settings()
									.getProperty("ldap-member-id");
		
		while( ouUsers.hasNext() )
		{
			SearchResultEntry ldapEntry = ouUsers.next();
		
			if( ! LDAPGroupUniqNames.isMember(ldapEntry.getAttributeValue( 
					uidStr )) )
			{
				_log.fine("not found in member list, removing: " 
						+ ldapEntry.getAttributeValue( uidStr ));
			
			}
			else
			{
				uidsLdapFormat.add( ldapEntry );
				
				_log.fine("in member list, adding: " 
						+ ldapEntry.getAttributeValue( uidStr ));
			
				Map<String,String> oneUser = new HashMap<>();
				
				Iterator<Attribute> userAttrs = ldapEntry.getAttributes()
						.iterator();
				
				while( userAttrs.hasNext() )
				{
					Attribute ldapAttribute = userAttrs.next(); 
		
					/**
					 * Alternatively, can filter after ldap search with BiMap:
					 * 
					 * if( DB2LDAPLayout.getLayout()
					 *				 	.values()
					 *				 	.contains(ldapAttribute) )
					 */

					oneUser.put( ldapAttribute.getBaseName(), 
							ldapAttribute.getValue() );
						
				}//while
				
				usersDbFormat.add( ImmutableMap.copyOf( oneUser ) );
								
			}//if
			
			_log.finer( "ldap ou search: " + ldapEntry );
						
		}//while

		checkNotNull( usersDbFormat, "null ldap users collection, db Map" );

		checkNotNull( uidsLdapFormat, "null ldap users collection, LDAP " +
				"Search Entry format" );
		
		checkArgument( usersDbFormat.size() > 0, "empty ldap users " + 
				"collection, db Map format" );

		checkArgument( uidsLdapFormat.size() > 0, "empty ldap users " + 
				"collection, LDAP Search Entry format" );
		
		_ldapOUusers = Optional.of( ImmutableList.copyOf( usersDbFormat ) );
		
		_ldapOUentries = Optional.of( ImmutableList.copyOf( uidsLdapFormat ));
		
		return _ldapOUusers.get();
		
	}//method
	
	
	/**
	 * 
	 * LDAP keys and corresponding values for OU (Organizational Unit).<p>
	 * 
	 * Only fields specified in database->LDAP layout are retrieved.<p>
	 * 
	 * Not all OU entries are retrieved: only LDAP entries in group of names 
	 * 	specified in XML Application Configuration.<p>
	 * 
	 * Only one value per key is supported, for one-to-one database query item 
	 * 	to LDAP.<p>
	 *
	 * Example:
	 * <table border=1>
	 *  <tr><td> LDAP key </td><td> LDAP value          </td></tr>
	 * 	<tr><td> cn    </td><td> Kevin                  </td></tr>
	 * 	<tr><td> sn    </td><td> Hatfield               </td></tr>
	 * 	<tr><td> uid   </td><td> 30004212               </td></tr>
	 *  <tr><td> mail  </td><td> hatfield_kp@mercer.edu </td></tr>
	 * 	<tr><td> telephoneNumber </td><td> 478.301.4132 </td></tr>
	 * 
	 * 	<tr><td> gidNumber       </td><td> 100             </td></tr>
	 * 
	 * </table>
	 * 
	 * @return ImmutableList&#60;ImmutableMap&#60;String,String&#62;&#62; - 
	 * 	read-only List of read-only Maps containing entries from LDAP OU
	 * 
	 * @throws GeneralSecurityException  error obtaining SSL socket for 
	 * 	secure communication with LDAP
	 * 
	 * @throws LDAPSearchException  OU not found in LDAP or other search 
	 * 	error
	 * 
	 * @throws LDAPException  invalid LDAP search syntax, LDAP connectivity 
	 * 	or other error
	 * 
	 * @throws SQLException  error accessing database for database->LDAP 
	 * 	layout
	 * 
	 * @see Config
	 * @see #getSearchResultEntries()
	 * @see DB2LDAPLayout
	 */
	
	public static ImmutableList<ImmutableMap<String,String>> getList() 
			throws GeneralSecurityException,
				   LDAPSearchException, 
				   LDAPException, 
				   SQLException
	{
		return _ldapOUusers.or( downloadOU() );
		
	}//method
	
	
	/**
	 * LDAP Search Entry objects for OU (Organizational Unit).<p>
	 * 
	 * @return ImmutableList&#60;SearchResultEntry&#62;  read-only List of 
	 * 	SearchResultEntries from LDAP OU
	 * 
	 * @throws GeneralSecurityException  error obtaining SSL socket for 
	 * 	secure communication with LDAP
	 * 
	 * @throws LDAPSearchException  OU not found in LDAP or other search 
	 * 	error
	 * 
	 * @throws LDAPException  invalid LDAP search syntax, LDAP connectivity 
	 * 	or other error
	 * 
	 * @throws SQLException  error accessing database for database->LDAP 
	 * 	layout
	 * 
	 * @see Config
	 * @see #getLDAPOU()
	 * @see DB2LDAPLayout 
	 * @see SearchResultEntry
	 */
	
	public static ImmutableList<SearchResultEntry> getSearchResultEntries() 
			throws GeneralSecurityException,
				   LDAPSearchException, 
				   LDAPException, 
				   SQLException
	{
		if( ! _ldapOUentries.isPresent() ) downloadOU();
		
		return _ldapOUentries.get();
		
	}//method
	
	
	public static void refresh()
	{
		_ldapOUentries = Optional.absent();
		_ldapOUusers = Optional.absent();
		
	}//method

	
}//class
